package com.magenta.maxoptra.gis.service;

import com.magenta.maxoptra.commons.db.DataSourceQueryExecuter;
import com.magenta.maxoptra.commons.db.QueryExecuter;
import com.magenta.maxoptra.commons.db.ResultSetCommand;
import com.magenta.maxoptra.gis.*;
import com.magenta.maxoptra.gis.commons.Logger;
import com.magenta.maxoptra.gis.commons.Pair;
import com.magenta.maxoptra.gis.commons.StringUtils;
import org.jboss.annotation.ejb.Management;
import org.jboss.annotation.ejb.Service;
import org.jboss.mq.server.jmx.QueueMBean;
import org.jboss.mx.util.MBeanServerLocator;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import javax.ejb.Local;
import javax.management.MBeanServerConnection;
import javax.management.MBeanServerInvocationHandler;
import javax.management.ObjectName;
import javax.sql.DataSource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.InputStream;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;

/*
* Project: MaxOptRA v1.2
* User: Ivan Usalko
* Date: 16.09.2009 12:47:19
*
* Copyright (c) 1999-2009 Magenta Corporation Ltd. All Rights Reserved.
* Magenta Technology proprietary and confidential.
* Use is subject to license terms.
*/
//@Stateless(description = "Registry of external geo-info providers.")
@Service(name = ProvidersRegistry.JNDI_NAME,
        objectName = "gis-services:service=ProvidersRegistry")
@Local(ProvidersRegistry.class)
@Management(ProvidersRegistryMBean.class)
public class GeoProvidersRegistry implements ProvidersRegistry, ProvidersRegistryMBean {

    private static final String DS_JNDI = "java:/maxoptra/datasource/routes-cache";


    public static final String NODE_ADAPTERS = "gis-adapters";
    public static final String NODE_ADAPTER = "gis-adapter";

    public static final String ATTR_CLASS_NAME = "className";
    public static final String ATTR_NAME = "name";
    public static final String ATTR_VERSION = "version";

    protected Logger logger;
    private Map<Long, Pair<String, String>> index;

    private QueryExecuter queryExecuter;

    /**
     * PROVIDER_NAME-VERSION_API -> CLASS NAME
     */
    private Map<String, String> classes;

    private Map<Long, CidToProvider> usedProviders;

    private List<GenericGeoProvider> geoProviderList = new LinkedList<GenericGeoProvider>();

    @Resource(mappedName = DS_JNDI, type = DataSource.class)
    private DataSource ds;

    public Pair<String, String> getProviderKey(Long connectionId) {
        Pair<String, String> key = index.get(connectionId);
        if (key == null) {
            logger.warn("PROVIDERS REGISTRY CONTAINS NULL BASED PROVIDER_KEY! [connection id = %s]", connectionId);
        }
        return key;
    }


    public List<Long> getConnections(String providerName, String versionApi) {
        final List<Long> connections;
        if (index.containsValue(providerName)) {
            connections = Collections.emptyList();
        } else {
            connections = new ArrayList<Long>();
        }
        for (Map.Entry<Long, Pair<String, String>> e : index.entrySet()) {
            if (providerName.equals(e.getValue())) {
                connections.add(e.getKey());
            }
        }
        return connections;
    }

    public void register(Long connectionId, String providerName, String versionApi) {
        index.put(connectionId, Pair.generate(providerName, versionApi));
    }

    @Override
    public void assignCID(Long cid, String providerName, String versionApi, String discriminator) {
        if (usedProviders.containsKey(cid)) {
            return;
        }

        final CidToProvider cidToProvider = new CidToProvider(cid, providerName, versionApi, discriminator);
        try {
            persistProvider(cidToProvider);
            usedProviders.put(cid, cidToProvider);
        } catch (Exception e) {
            logger.error("Can't assign CID {} to geo provider: {}, version {}, discriminator {}",
                    e, cid, providerName, versionApi, discriminator);
        }
    }

    public String getProviders() {
        StringBuilder str = new StringBuilder();
        str.append("<b>").append("Providers list:").append("</b>").append("<br><ul>");
        for (GenericGeoProvider provider : geoProviderList) {
            str.append("<li>").append(provider.getProviderName());
            str.append(provider.getVersion() != null ? " v." + provider.getVersion() : "");
            str.append("</li>");
        }
        str.append("</ul><br>");
        str.append("Used providers list").append("<ul>");
        for (CidToProvider provider : usedProviders.values()) {
            str.append("<li>").append(provider.cid).append(": ");
            str.append(provider.providerName);
            str.append(provider.versionApi != null ? " v." + provider.versionApi : "");
            str.append(" - ").append(provider.discriminator);
            str.append("</li>");
        }
        str.append("</ul>");
        return str.toString();
    }

    @PostConstruct
    public void init() {
        logger = new Logger(getClass());
        index = new HashMap<Long, Pair<String, String>>();
        classes = new HashMap<String, String>();
        usedProviders = new HashMap<Long, CidToProvider>();

        InputStream is = getClass().getClassLoader().getResourceAsStream("gis-adapters.xml");
        DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
        Document doc = null;
        Map<String, Map<String, GeoStatusCode>> commonProviderErrorMap = new HashMap<String, Map<String, GeoStatusCode>>();
        try {
            try {
                docBuilderFactory.setValidating(false);
                DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
                doc = docBuilder.parse(is);
            } catch (ParserConfigurationException e) {
                logger.error(e);
            }
            doc.getDocumentElement().normalize();
            Element rootEl = doc.getDocumentElement();
            geoProviderList.clear();
            NodeList listOfAdaptersEl = rootEl.getElementsByTagName(NODE_ADAPTER);
            if (listOfAdaptersEl != null) {
                for (int s = 0; s < listOfAdaptersEl.getLength(); s++) {
                    Node node = listOfAdaptersEl.item(s);
                    if (node.getNodeType() == Node.ELEMENT_NODE) {
                        Element element = (Element) node;
                        String version = element.getAttribute(ATTR_VERSION);
                        String className = element.getAttribute(ATTR_CLASS_NAME);
                        IGeoProvider adapterClass = getGeoProviderClass(className);
                        if ( adapterClass == null ) {//IGNORE ADAPTER
                        	logger.warn("Ignore adapter [%s] with version [%s], by reason: class [%s] not found!", element.getAttribute(ATTR_NAME), version, className);
                        	continue;
                        }
                        Map<String, GeoStatusCode> providerErrorMap = adapterClass.getProviderErrorMap();
                        GenericGeoProvider geoProvider = new GenericGeoProvider(
                                element.getAttribute(ATTR_NAME),
                                (StringUtils.isNotEmpty(version) ? version : null),
                                className,
                                providerErrorMap
                        );
                        if (providerErrorMap != null && providerErrorMap.size() > 0) {
                            commonProviderErrorMap.put(geoProvider.getId(), providerErrorMap);
                        }
                        geoProviderList.add(geoProvider);
                        classes.put(geoProvider.getId(), geoProvider.getClassName());
                    }
                }
            }
        } catch (Exception e) {
            logger.error("Cannot read script from resources", e);
        }

        queryExecuter = new DataSourceQueryExecuter(ds);

        for (GeoStatusCode geoStatusCode : GeoStatusCode.values()) {
            persistGeoStatusCode(geoStatusCode);
        }

        for (String id : commonProviderErrorMap.keySet()) {
            Map<String, GeoStatusCode> providerErrorMap = commonProviderErrorMap.get(id);
            for (Map.Entry<String, GeoStatusCode> entry : providerErrorMap.entrySet()) {
                persistProviderErrorCode(id, entry.getKey(), entry.getValue().getCode());
            }
        }

        loadUsedProviders();
    }

    private <T extends IGeoProvider> T getGeoProviderClass(String className) {
        Object connector = null;
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        try {
            Class<?> connectorClass = cl.loadClass(className);
            connector = connectorClass.newInstance();
        } catch (ClassNotFoundException e) {
            logger.error("WRONG CLASS " + className + "FOR PROVIDER IN CONNECTION PARAMS", e);
        } catch (InstantiationException e) {
            logger.error("WRONG CLASS " + className + "FOR PROVIDER IN CONNECTION PARAMS", e);
        } catch (IllegalAccessException e) {
            logger.error("WRONG CLASS " + className + "FOR PROVIDER IN CONNECTION PARAMS", e);
        }
        return (T) connector;
    }

    private void loadUsedProviders() {
        try {
            queryExecuter.iterateResultSet("select * from providers",
                    new ResultSetCommand() {
                        @Override
                        public void execute(ResultSet resultSet) throws SQLException {
                            int columnCount = resultSet.getMetaData().getColumnCount();
                            Object[] values = new Object[columnCount];
                            for (int i = 0; i < columnCount; i++) {
                                values[i] = resultSet.getObject(i + 1);
                            }
                            Long cid = (Long) values[0];
                            usedProviders.put(cid, new CidToProvider(cid, (String) values[1], (String) values[2], (String) values[3]));
                        }

                        @Override
                        public int getMaxResults() {
                            return 0;
                        }

                        @Override
                        public int getFetchSize() {
                            return 0;
                        }
                    });
        } catch (SQLException e) {
            logger.error("Can't read list of known geo providers", e);
        }
    }

    private void persistProvider(CidToProvider provider) {
        Object[] paramsValues = new Object[]{provider.cid, provider.providerName, provider.versionApi, provider.discriminator};
        try {
            boolean recordExists = queryExecuter
                    .resultExists("select 1 from providers where cid=? and name=? and version=? and discriminator=?", paramsValues);
            if (!recordExists) {
                String sqlQuery = "insert into providers (cid, name, version, discriminator) values (?, ?, ?, ?)";
                queryExecuter.executeUpdate(sqlQuery, paramsValues);
            }
        } catch (SQLException e) {
            logger.error("Cannot save in DB info about " + provider.providerName);
        }
    }

    private void persistProviderErrorCode(String id, String providerErrorCode, int commonErrorCode) {
        Object[] paramsValues = new Object[3];
        paramsValues[0] = id;
        paramsValues[1] = providerErrorCode;
        paramsValues[2] = commonErrorCode;
        boolean recordExists = false;
        try {
            recordExists = queryExecuter.resultExists("select 1 from provider_error_codes " +
                    "where cid = ? " +
                    "and providerErrorCode = ? " +
                    "and errorCode = ?", paramsValues);
            if (!recordExists) {
                String sqlQuery = "insert into provider_error_codes (cid, providerErrorCode, errorCode) values (?, ?, ?)";
                queryExecuter.executeUpdate(sqlQuery, paramsValues);
            }
        } catch (SQLException e) {
            logger.error("Cannot save in DB info about providerErrorCode:" + providerErrorCode + ", commonErrorCode=" + commonErrorCode);
        }
    }

    private void persistGeoStatusCode(GeoStatusCode geoStatusCode) {
        Object[] paramsValues = new Object[1];
        paramsValues[0] = geoStatusCode.getCode();
        boolean recordExists = false;
        try {
            recordExists = queryExecuter.resultExists("select 1 from error_codes where code = ?", paramsValues);
            if (!recordExists) {
                paramsValues = new Object[2];
                paramsValues[0] = geoStatusCode.getCode();
                paramsValues[1] = geoStatusCode.getDescription();
                String sqlQuery = "insert into error_codes (code, name) values (?, ?)";
                queryExecuter.executeUpdate(sqlQuery, paramsValues);
            }
        } catch (SQLException e) {
            logger.error("Cannot save in DB info about " + geoStatusCode);
        }
    }

    @PreDestroy
    public void release() {
        try {
            //REMOVE ALL MESSAGES (REQUESTS TO EXTERNAL PROVIDERS)
            String queueName = "gis-services-route-requests";
            MBeanServerConnection connection = MBeanServerLocator.locateJBoss();
            QueueMBean giServicesRouteRequestsQueue = MBeanServerInvocationHandler.newProxyInstance(connection,
                    new ObjectName("jboss.mq.destination:service=Queue,name=" + queueName),
                    QueueMBean.class, true);
            giServicesRouteRequestsQueue.removeAllMessages();
            if (giServicesRouteRequestsQueue.getQueueDepth() == 0) {
                logger.info("REMOVE ALL MESSAGES FOR QUEUE [" + queueName + "] SUCCESS!");
            }
        } catch (Exception e) {
            logger.error("REMOVE ALL MESSAGES (REQUESTS TO EXTERNAL PROVIDERS) FAILED!", e);
        }

        index.clear();
    }

    public void start() {
        if (index == null) {
            init();
        }
//        LoggingUtil.reconfigure("gis-services");
    }

    public void stop() {
        release();
    }

    @Override
    public String getImplementor(String providerName, String versionApi) {
        return classes.get(makeProviderId(providerName, versionApi));
    }

    private static final class CidToProvider {
        public final Long cid;
        public final String providerName;
        public final String versionApi;
        public final String discriminator;

        private CidToProvider(Long cid, String providerName, String versionApi, String discriminator) {
            this.cid = cid;
            this.providerName = providerName;
            this.versionApi = versionApi;
            this.discriminator = discriminator;
        }
    }

    class GenericGeoProvider implements IGeoProvider {

        private String providerName;
        private String version;
        private String className;
        private Map<String, GeoStatusCode> providerErrorMap;

        public GenericGeoProvider(String providerName, String version, String className, Map<String, GeoStatusCode> providerErrorMap) {
            this.providerName = providerName;
            this.version = version;
            this.className = className;
            this.providerErrorMap = providerErrorMap;
        }

        public String getProviderName() {
            return providerName;
        }

        public void setProviderName(String providerName) {
            this.providerName = providerName;
        }

        public String getVersion() {
            return version;
        }

        public void setVersion(String version) {
            this.version = version;
        }

        public String getClassName() {
            return className;
        }

        @Override
        public Map<String, GeoStatusCode> getProviderErrorMap() {
            return providerErrorMap;
        }

        @Override
        public Boolean getGisBooleanProperty(GisParameter paramater) {
            return null;
        }

        @Override
        public String getGisStringProperty(GisParameter parameter) {
            return null;
        }

        @Override
        public Integer getGisIntegerProperty(GisParameter parameter) {
            return null;
        }

        @Override
        public Float getGisFloatProperty(GisParameter parameter) {
            return null;
        }

        public void setClassName(String className) {
            this.className = className;
        }

        public String getId() {
            return makeProviderId(providerName, version);
        }
    }

    public static String makeProviderId(String providerName, String versionApi) {
        return (providerName + "-" + versionApi).toLowerCase();
    }
}